diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index b7d68644..7c0fce92 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -27,10 +27,10 @@ jobs: run: go build -v ./cmd/main.go - name: Test - run: go test -coverprofile=coverage.out -v ./... + run: make test-unit - name: Test for Data Race - run: go test ./... --race + run: make test-race - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v4.0.1 diff --git a/.gitignore b/.gitignore index 7c4c1304..6bb2ab16 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ volumes/nodes dist/ pkg/modules/*/aof pkg/echovault/aof -dump.rdb \ No newline at end of file +dump.rdb +**/*/testdata \ No newline at end of file diff --git a/Makefile b/Makefile index bc682687..e9607b5a 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ 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 + 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: CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOOS=$(GOOS) GOARCH=$(GOARCH) go build -o $(DEST)/server ./cmd/main.go @@ -13,7 +13,13 @@ run: make build && docker-compose up --build test-unit: - go clean -testcache && go test ./... -coverprofile coverage/coverage.out + CGO_ENABLED=1 go build -buildmode=plugin -o internal/modules/admin/testdata/modules/module_set/module_set.so ./volumes/modules/module_set/module_set.go && \ + CGO_ENABLED=1 go build -buildmode=plugin -o internal/modules/admin/testdata/modules/module_get/module_get.so ./volumes/modules/module_get/module_get.go && \ + go clean -testcache && \ + CGO_ENABLED=1 go test ./... -coverprofile coverage/coverage.out test-race: - go clean -testcache && go test ./... --race \ No newline at end of file + CGO_ENABLED=1 go build -buildmode=plugin --race -o internal/modules/admin/testdata/modules/module_set/module_set.so ./volumes/modules/module_set/module_set.go && \ + CGO_ENABLED=1 go build -buildmode=plugin --race -o internal/modules/admin/testdata/modules/module_get/module_get.so ./volumes/modules/module_get/module_get.go && \ + go clean -testcache && \ + CGO_ENABLED=1 go test ./... --race \ No newline at end of file diff --git a/README.md b/README.md index 23dada5f..119cac30 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ capability is always being worked on and improved. # Features -Some key features offered by EchoVault include: +Features offered by EchoVault include: 1) TLS and mTLS support for multiple server and client RootCAs. 2) Replication cluster support using the RAFT algorithm. @@ -43,19 +43,20 @@ Some key features offered by EchoVault include: 5) Sets, Sorted Sets, Hashes, Lists and more. 6) Persistence layer with Snapshots and Append-Only files. 7) Key Eviction Policies. +8) Command extension via shared object files. +9) Command extension via embedded API. We are working hard to add more features to EchoVault to make it much more powerful. Features in the roadmap include: 1) Sharding -2) Shared Object File Plugins -3) Streams -4) Transactions -5) Bitmap -6) HyperLogLog -7) Lua Modules -8) JSON -9) Improved Observability +2) Streams +3) Transactions +4) Bitmap +5) HyperLogLog +6) Lua Modules +7) JSON +8) Improved Observability # Usage (Embedded) @@ -68,25 +69,21 @@ You can access all of EchoVault's commands using an ergonomic API. ```go func main() { - server, err := echovault.NewEchoVault( - echovault.WithConfig(config.DefaultConfig()), - echovault.WithCommands(commands.All()), - ) + server, err := echovault.NewEchoVault() if err != nil { log.Fatal(err) } - _, _ = server.SET("key", "Hello, world!", echovault.SETOptions{}) - - v, _ := server.GET("key") - + _, _ = server.Set("key", "Hello, world!", echovault.SETOptions{}) + + v, _ := server.Get("key") fmt.Println(v) // Hello, world! wg := sync.WaitGroup{} // Subscribe to multiple EchoVault channels. - readMessage := server.SUBSCRIBE("subscriber1", "channel_1", "channel_2", "channel_3") + readMessage := server.Subscribe("subscriber1", "channel_1", "channel_2", "channel_3") wg.Add(1) go func() { wg.Done() @@ -103,7 +100,7 @@ func main() { // Simulating delay. <-time.After(1 * time.Second) // Publish message to each EchoVault channel. - _, _ = server.PUBLISH(fmt.Sprintf("channel_%d", i), "Hello!") + _, _ = server.Publish(fmt.Sprintf("channel_%d", i), "Hello!") } wg.Done() }() @@ -128,195 +125,20 @@ To install via homebrew, run: Once installed, you can run the server with the following command: `echovault --bind-addr=localhost --data-dir="path/to/persistence/directory"` -Next, [install the client via homebrew](https://github.com/EchoVault/EchoVault-CLI). - ### Binaries You can download the binaries by clicking on a release tag and downloading the binary for your system. -Checkout the [configuration section](#configuration) for the possible configuration -flags. - # Clients EchoVault uses RESP, which makes it compatible with existing Redis clients. -# Development Setup - -Pre-requisites: -1) Go -2) Docker -3) Docker Compose -4) x86_64-linux-musl-gcc cross-compile toolchain as the development image is built for an Alpine container - -Steps: -1) Clone the repository. -2) If you're on MacOS, you can run `make build && docker-compose up --build` to build the project and spin up the development docker container. -3) If you're on another OS, you will have to use `go build` with the relevant flags for your system. - -# Table of Contents -1. [Configuration](#configuration) -2. [Eviction](#eviction) -3. [Contribution](#contribution) - -# Configuration - -EchoVault is highly configurable. It provides the following configuration options to you: - -Flag: `--config`
-Type: `string/path`
-Description: The file path for the server configuration. A JSON or YAML file can be used for server configuration. You can combine CLI flags and config files, but remember that config files override CLI flags. The config file will be prioritised if you have the same config option in the CLI flags and the config file. - -Flag: `--port`
-Type: `integer`
-Description: The port on which to listen to client connections. The default is `7480`. - -Flag: `--bind-addr`
-Type: `string`
-Description: Specify the IP address to which the listener is bound. - -Flag: `--require-pass`
-Type: `boolean`
-Description: Determines whether the server should require a password for the default user before allowing commands. The default is `false`. If this option is provided, it must be accompanied by the `--password` config. - -Flag: `--password`
-Type: `string`
-Description: The password used to authorize the default user to run commands. This flag should be provided alongside the `--require-pass` flag. - -Flag: `--tls`
-Type: `boolean`
-Description: A TLS connection with a client is required. The default is `false`. - -Flag: `mtls`
-Type: `boolean`
-Description: Require mTLS connection with client. It is useful when the client and the server need to verify each other. If `--tls` and `mtls` are provided, `--mtls` will take higher priority. The default is `false`. - -Flag: `--cert-key-pair`
-Type: `string`
-Description: The cert/key pair used by the server to authenticate itself to the client when using TLS or mTLS. This flag can be provided multiple times with multiple cert/key pairs. This is a comma-separated string in the following format: `,`, - -Flag: `--client-ca`
-Type: `string`
-Description: The path to the RootCA that is used to verify client certs when the `--mtls` flag is provided to enable verifying the client. This flag can be passed multiple times with paths to several client RootCAs. - -Flag: `--server-id`
-Type: `string`
-Description: If this node is part of a raft replication cluster, then this flag provides the server ID to use within the cluster configuration. This ID must be unique to all the other nodes' IDs in the cluster. - -Flag: `--join-addr`
-Type: `string`
-Description: When adding a node to a replication cluster, this is the address and port of any cluster member. The current node will use this to request permission to join the cluster. The format of this flag is `:`. - -Flag: `--raft-port`
-Type: `integer`
-Description: If starting a node in a raft replication cluster, this port will be used for communication between nodes on the raft layer. The default is `7481`. - -Flag: `--memberlist-port`
-Type: `integer`
-Description. If starting a node in a replication cluster, this port is used for communication between nodes on the memberlist layer. The default is `7946`. - -Flag: `--in-memory`
-Type: `boolean`
-Description: When starting a node in a raft replication cluster, this directs the raft layer to store logs and snapshots in memory. It is only recommended in test mode. The default is `false`. - -Flag: `--data-dir`
-Type: `string`
-Description: The directory for storing Append-Only Logs, Write Ahead Logs, and Snapshots. The default is `/var/lib/echovault` - -Flag: `--bootstrap-cluster`
-Type: `boolean`
-Description: Whether to initialize a new replication cluster with this node as the leader. The default is `false`. - -Flag: `--acl-config`
-Type: `string`
-Description: The file path for the ACL layer config file. The ACL configuration file can be a YAML or JSON file. - -Flag: `--snapshot-threshold`
-Type: `integer`
-Description: The number of write commands required to trigger a snapshot. The default is `1,000` - -Flag: `--snapshot-interval`
-Type: `string`
-Description: The interval between snapshots. You can provide a parseable time format such as `30m45s` or `1h45m`. The default is 5 minutes. - -Flag: `--restore-snapshot`
-Type: `boolean`
-Description: Determines whether to restore from a snapshot on startup. The default is `false`. - -Flag: `--restore-aof`
-Type: `boolean`
-Description: This flag determines whether to restore from an aof file on startup. If both this flag and `--restore-snapshot` are provided, this flag will take higher priority. - -Flag: `--forward-commands`
-Type: `boolean`
-Description: This flag allows you to send write commands to any node in the cluster. The node will forward the command to the cluster leader. When this is false, write commands can only be accepted by the leader. The default is `false`. - -Flag: `--max-memory`
-Type: `string`
-Examples: "200mb", "8gb", "1tb"
-Description: The maximum memory usage that EchoVault should observe. Once this limit is reached, the chosen key eviction strategy is triggered. The default is no limit. - -Flag: `--eviction-policy`
-Type: `string`
-Description: This flag allows you to choose the key eviction strategy when the maximum memory is reached. The flag accepts the following options:
-1) noeviction - Do not evict any keys even when max-memory is exceeded. All new write operations will be rejected. This is the default eviction strategy. -2) allkeys-lfu - Evict the least frequently used keys when max-memory is exceeded. -3) allkeys-lru - Evict the least recently used keys when max-memory is exceeded. -4) volatile-lfu - Evict the least frequently used keys with an expiration when max-memory is exceeded. -5) volatile-lru - Evict the least recently used keys with an expiration when max-memory is exceeded. -6) allkeys-random - Evict random keys until we get under the max-memory limit when max-memory is exceeded. -7) volatile-random - Evict random keys with an expiration when max-memory is exceeded. - -Flag: `--eviction-sample`
-Type: `integer`
-Description: An integer specifying the number of keys to sample when checking for expired keys. By default, EchoVault will sample 20 keys. The sampling is repeated if the number of expired keys found exceeds 20%. - -Flag: `--eviction-interval`
-Type: `string`
-Example: "10s", "5m30s", "100ms"
-Description: The interval between each sampling of keys to evict. By default, this happens every 100 milliseconds. - -# Eviction - -### Memory Limit -The memory limit can be set using the `--max-memory` config flag. This flag accepts a parsable memory value (e.g 100mb, 16gb). If the limit set is 0, then no memory limit is imposed. The default value is 0. - -### Passive eviction -In passive eviction, the expired key is not deleted immediately after the expiry time. The key will remain in the store until the next time it is accessed. When attempting to access an expired key, that is when the key is deleted. - -### Active eviction -Echovault will run a background goroutine that samples a set of volatile keys at a given interval. Any keys that are found to be expired will be deleted. If 20% or more of the sampled keys are deleted, then the process will immediately begin again. Otherwise, wait for the given interval until the next round of sampling/eviction. The default number of keys sampled is 20, and the default interval for sampling is 100 milliseconds. These can be configured using the `--eviction-sample` and `--eviction-interval` flags. - -### Eviction Policies -Eviction policy can be set using the --eviction-policy flag. The following options are available. - -noeviction:
-This policy does not evict any keys. When max memory is reached, all new write commands will be rejected until keys are manually deleted by the user. - -allkeys-lfu:
-With this policy, all keys are considered for eviction when the max memory is reached. When max memory is reached, the least frequently accessed keys will be evicted until the memory usage is under the memory limit. - -allkeys-lru:
-This policy will consider all keys for eviction when max memory is reached. The least recently accessed keys will be deleted one by one until we are below the memory limit. - -allkeys-random:
-Evict random keys until we're below the max memory limit. - -volatile-lfu:
-With this policy, only keys with an associated expiry time will be evicted to adhere to the memory limit. When the memory limit is exceeded, volatile keys will be evicted starting from the least frequently used until we are below the memory limit or are out of volatile keys to evict. +# Documentation -volatile-lru:
-With this policy, only keys with an associated expiry time will be evicted to adhere to the memory limit. When the memory limit is exceeded, volatile keys will be evicted starting from the list recently used until we are below the memory limit or are out of volatile keys to evict. +https://echovault.io -volatile-random:
-Evict random volatile keys until we're below the memory limit, or we're out of volatile keys to evict. -# Contribution -Contributions are welcome! If you're interested in contributing, -feel free to clone the repository and submit a Pull Request. -Join the [discord server](https://discord.gg/JrG4kPrF8v) if you'd like to discuss your contribution and/or -be a part of the community. diff --git a/coverage/coverage.out b/coverage/coverage.out index 4a8f2112..db263395 100644 --- a/coverage/coverage.out +++ b/coverage/coverage.out @@ -1014,48 +1014,50 @@ github.com/echovault/echovault/echovault/modules.go:131.3,131.18 1 0 github.com/echovault/echovault/echovault/modules.go:135.2,135.34 1 0 github.com/echovault/echovault/echovault/modules.go:135.34,138.3 2 0 github.com/echovault/echovault/echovault/modules.go:140.2,140.72 1 0 -github.com/echovault/echovault/echovault/plugin.go:35.72,37.16 2 0 -github.com/echovault/echovault/echovault/plugin.go:37.16,39.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:41.2,42.16 2 0 -github.com/echovault/echovault/echovault/plugin.go:42.16,44.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:45.2,46.9 2 0 -github.com/echovault/echovault/echovault/plugin.go:46.9,48.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:50.2,51.16 2 0 -github.com/echovault/echovault/echovault/plugin.go:51.16,53.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:54.2,55.9 2 0 -github.com/echovault/echovault/echovault/plugin.go:55.9,57.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:59.2,60.16 2 0 -github.com/echovault/echovault/echovault/plugin.go:60.16,62.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:63.2,64.9 2 0 -github.com/echovault/echovault/echovault/plugin.go:64.9,66.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:68.2,69.16 2 0 -github.com/echovault/echovault/echovault/plugin.go:69.16,71.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:72.2,73.9 2 0 -github.com/echovault/echovault/echovault/plugin.go:73.9,75.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:77.2,78.16 2 0 -github.com/echovault/echovault/echovault/plugin.go:78.16,80.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:81.2,82.9 2 0 -github.com/echovault/echovault/echovault/plugin.go:82.9,84.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:86.2,87.16 2 0 -github.com/echovault/echovault/echovault/plugin.go:87.16,89.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:90.2,103.9 2 0 -github.com/echovault/echovault/echovault/plugin.go:103.9,105.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:107.2,113.31 3 0 -github.com/echovault/echovault/echovault/plugin.go:113.31,116.36 2 0 -github.com/echovault/echovault/echovault/plugin.go:116.36,118.5 1 0 -github.com/echovault/echovault/echovault/plugin.go:119.4,119.15 1 0 -github.com/echovault/echovault/echovault/plugin.go:124.83,126.18 2 0 -github.com/echovault/echovault/echovault/plugin.go:126.18,128.5 1 0 -github.com/echovault/echovault/echovault/plugin.go:129.4,133.10 1 0 -github.com/echovault/echovault/echovault/plugin.go:135.72,148.4 1 0 -github.com/echovault/echovault/echovault/plugin.go:151.2,151.12 1 0 -github.com/echovault/echovault/echovault/plugin.go:159.54,162.91 3 0 -github.com/echovault/echovault/echovault/plugin.go:162.91,164.3 1 0 -github.com/echovault/echovault/echovault/plugin.go:170.49,174.42 4 0 -github.com/echovault/echovault/echovault/plugin.go:174.42,175.61 1 0 -github.com/echovault/echovault/echovault/plugin.go:175.61,177.4 1 0 -github.com/echovault/echovault/echovault/plugin.go:177.6,179.4 1 0 -github.com/echovault/echovault/echovault/plugin.go:181.2,181.16 1 0 +github.com/echovault/echovault/echovault/plugin.go:35.72,40.16 4 0 +github.com/echovault/echovault/echovault/plugin.go:40.16,42.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:44.2,45.16 2 0 +github.com/echovault/echovault/echovault/plugin.go:45.16,47.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:48.2,49.9 2 0 +github.com/echovault/echovault/echovault/plugin.go:49.9,51.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:53.2,54.16 2 0 +github.com/echovault/echovault/echovault/plugin.go:54.16,56.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:57.2,58.9 2 0 +github.com/echovault/echovault/echovault/plugin.go:58.9,60.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:62.2,63.16 2 0 +github.com/echovault/echovault/echovault/plugin.go:63.16,65.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:66.2,67.9 2 0 +github.com/echovault/echovault/echovault/plugin.go:67.9,69.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:71.2,72.16 2 0 +github.com/echovault/echovault/echovault/plugin.go:72.16,74.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:75.2,76.9 2 0 +github.com/echovault/echovault/echovault/plugin.go:76.9,78.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:80.2,81.16 2 0 +github.com/echovault/echovault/echovault/plugin.go:81.16,83.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:84.2,85.9 2 0 +github.com/echovault/echovault/echovault/plugin.go:85.9,87.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:89.2,90.16 2 0 +github.com/echovault/echovault/echovault/plugin.go:90.16,92.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:93.2,106.9 2 0 +github.com/echovault/echovault/echovault/plugin.go:106.9,108.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:111.2,111.91 1 0 +github.com/echovault/echovault/echovault/plugin.go:111.91,113.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:116.2,119.31 1 0 +github.com/echovault/echovault/echovault/plugin.go:119.31,122.36 2 0 +github.com/echovault/echovault/echovault/plugin.go:122.36,124.5 1 0 +github.com/echovault/echovault/echovault/plugin.go:125.4,125.15 1 0 +github.com/echovault/echovault/echovault/plugin.go:130.83,132.18 2 0 +github.com/echovault/echovault/echovault/plugin.go:132.18,134.5 1 0 +github.com/echovault/echovault/echovault/plugin.go:135.4,139.10 1 0 +github.com/echovault/echovault/echovault/plugin.go:141.72,155.4 1 0 +github.com/echovault/echovault/echovault/plugin.go:158.2,158.12 1 0 +github.com/echovault/echovault/echovault/plugin.go:166.54,169.91 3 0 +github.com/echovault/echovault/echovault/plugin.go:169.91,171.3 1 0 +github.com/echovault/echovault/echovault/plugin.go:177.49,181.42 4 0 +github.com/echovault/echovault/echovault/plugin.go:181.42,182.61 1 0 +github.com/echovault/echovault/echovault/plugin.go:182.61,184.4 1 0 +github.com/echovault/echovault/echovault/plugin.go:184.6,186.4 1 0 +github.com/echovault/echovault/echovault/plugin.go:188.2,188.16 1 0 github.com/echovault/echovault/echovault/test_helpers.go:9.35,16.2 2 1 github.com/echovault/echovault/echovault/test_helpers.go:18.95,19.61 1 1 github.com/echovault/echovault/echovault/test_helpers.go:19.61,21.3 1 0 @@ -1063,6 +1065,54 @@ github.com/echovault/echovault/echovault/test_helpers.go:22.2,22.57 1 1 github.com/echovault/echovault/echovault/test_helpers.go:22.57,24.3 1 0 github.com/echovault/echovault/echovault/test_helpers.go:25.2,26.12 2 1 github.com/echovault/echovault/echovault/test_helpers.go:29.95,34.2 4 1 +github.com/echovault/echovault/internal/aof/engine.go:50.56,51.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:51.30,53.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:56.57,57.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:57.30,59.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:62.59,63.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:63.30,65.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:68.58,69.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:69.30,71.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:74.59,75.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:75.30,77.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:80.82,81.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:81.30,83.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:86.89,87.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:87.30,89.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:92.73,93.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:93.30,95.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:98.82,99.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:99.30,101.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:104.78,105.30 1 1 +github.com/echovault/echovault/internal/aof/engine.go:105.30,107.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:110.69,118.29 1 1 +github.com/echovault/echovault/internal/aof/engine.go:118.30,118.31 0 0 +github.com/echovault/echovault/internal/aof/engine.go:119.30,119.31 0 0 +github.com/echovault/echovault/internal/aof/engine.go:120.57,120.71 1 0 +github.com/echovault/echovault/internal/aof/engine.go:121.63,121.64 0 0 +github.com/echovault/echovault/internal/aof/engine.go:122.44,122.45 0 0 +github.com/echovault/echovault/internal/aof/engine.go:127.2,127.33 1 1 +github.com/echovault/echovault/internal/aof/engine.go:127.33,129.3 1 1 +github.com/echovault/echovault/internal/aof/engine.go:132.2,139.16 2 1 +github.com/echovault/echovault/internal/aof/engine.go:139.16,141.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:142.2,152.16 3 1 +github.com/echovault/echovault/internal/aof/engine.go:152.16,154.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:155.2,159.12 2 1 +github.com/echovault/echovault/internal/aof/engine.go:159.12,160.7 1 1 +github.com/echovault/echovault/internal/aof/engine.go:160.7,162.54 2 1 +github.com/echovault/echovault/internal/aof/engine.go:162.54,164.5 1 0 +github.com/echovault/echovault/internal/aof/engine.go:168.2,168.20 1 1 +github.com/echovault/echovault/internal/aof/engine.go:171.52,173.2 1 1 +github.com/echovault/echovault/internal/aof/engine.go:175.42,183.62 5 1 +github.com/echovault/echovault/internal/aof/engine.go:183.62,185.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:188.2,188.54 1 1 +github.com/echovault/echovault/internal/aof/engine.go:188.54,190.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:192.2,192.12 1 1 +github.com/echovault/echovault/internal/aof/engine.go:195.39,196.55 1 1 +github.com/echovault/echovault/internal/aof/engine.go:196.55,198.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:199.2,199.53 1 1 +github.com/echovault/echovault/internal/aof/engine.go:199.53,201.3 1 0 +github.com/echovault/echovault/internal/aof/engine.go:202.2,202.12 1 1 github.com/echovault/echovault/internal/aof/log/store.go:46.60,47.34 1 1 github.com/echovault/echovault/internal/aof/log/store.go:47.34,49.3 1 1 github.com/echovault/echovault/internal/aof/log/store.go:52.61,53.34 1 1 @@ -1118,54 +1168,6 @@ github.com/echovault/echovault/internal/aof/log/store.go:191.2,191.47 1 0 github.com/echovault/echovault/internal/aof/log/store.go:191.47,193.3 1 0 github.com/echovault/echovault/internal/aof/log/store.go:194.2,194.12 1 0 github.com/echovault/echovault/internal/aof/log/store.go:197.41,201.2 3 1 -github.com/echovault/echovault/internal/aof/engine.go:50.56,51.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:51.30,53.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:56.57,57.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:57.30,59.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:62.59,63.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:63.30,65.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:68.58,69.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:69.30,71.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:74.59,75.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:75.30,77.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:80.82,81.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:81.30,83.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:86.89,87.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:87.30,89.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:92.73,93.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:93.30,95.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:98.82,99.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:99.30,101.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:104.78,105.30 1 1 -github.com/echovault/echovault/internal/aof/engine.go:105.30,107.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:110.69,118.29 1 1 -github.com/echovault/echovault/internal/aof/engine.go:118.30,118.31 0 0 -github.com/echovault/echovault/internal/aof/engine.go:119.30,119.31 0 0 -github.com/echovault/echovault/internal/aof/engine.go:120.57,120.71 1 0 -github.com/echovault/echovault/internal/aof/engine.go:121.63,121.64 0 0 -github.com/echovault/echovault/internal/aof/engine.go:122.44,122.45 0 0 -github.com/echovault/echovault/internal/aof/engine.go:127.2,127.33 1 1 -github.com/echovault/echovault/internal/aof/engine.go:127.33,129.3 1 1 -github.com/echovault/echovault/internal/aof/engine.go:132.2,139.16 2 1 -github.com/echovault/echovault/internal/aof/engine.go:139.16,141.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:142.2,152.16 3 1 -github.com/echovault/echovault/internal/aof/engine.go:152.16,154.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:155.2,159.12 2 1 -github.com/echovault/echovault/internal/aof/engine.go:159.12,160.7 1 1 -github.com/echovault/echovault/internal/aof/engine.go:160.7,162.54 2 1 -github.com/echovault/echovault/internal/aof/engine.go:162.54,164.5 1 0 -github.com/echovault/echovault/internal/aof/engine.go:168.2,168.20 1 1 -github.com/echovault/echovault/internal/aof/engine.go:171.52,173.2 1 1 -github.com/echovault/echovault/internal/aof/engine.go:175.42,183.62 5 1 -github.com/echovault/echovault/internal/aof/engine.go:183.62,185.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:188.2,188.54 1 1 -github.com/echovault/echovault/internal/aof/engine.go:188.54,190.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:192.2,192.12 1 1 -github.com/echovault/echovault/internal/aof/engine.go:195.39,196.55 1 1 -github.com/echovault/echovault/internal/aof/engine.go:196.55,198.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:199.2,199.53 1 1 -github.com/echovault/echovault/internal/aof/engine.go:199.53,201.3 1 0 -github.com/echovault/echovault/internal/aof/engine.go:202.2,202.12 1 1 github.com/echovault/echovault/internal/eviction/lfu.go:35.29,42.2 3 1 github.com/echovault/echovault/internal/eviction/lfu.go:44.34,46.2 1 1 github.com/echovault/echovault/internal/eviction/lfu.go:48.44,50.54 1 1 @@ -1637,99 +1639,6 @@ github.com/echovault/echovault/internal/modules/acl/user.go:289.40,304.2 1 1 github.com/echovault/echovault/internal/modules/acl/user.go:306.46,307.24 1 1 github.com/echovault/echovault/internal/modules/acl/user.go:307.24,309.3 1 1 github.com/echovault/echovault/internal/modules/acl/user.go:310.2,310.26 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:27.78,33.29 4 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:33.29,34.54 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:34.54,40.42 4 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:40.42,42.5 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:44.4,47.12 3 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:50.3,50.36 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:50.36,57.43 5 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:57.43,59.5 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:61.4,63.21 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:67.2,69.25 2 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:72.76,76.35 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:76.35,77.65 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:77.65,78.41 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:78.41,80.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:81.4,81.12 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:83.3,83.13 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:86.2,86.51 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:89.75,90.29 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:91.9,96.36 4 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:96.36,97.66 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:97.66,98.52 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:98.52,102.6 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:103.5,103.13 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:105.4,106.14 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:108.3,109.26 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:111.9,115.56 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:115.56,117.4 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:118.3,118.53 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:118.53,122.37 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:122.37,123.67 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:123.67,124.53 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:124.53,125.59 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:125.59,129.8 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:131.6,131.14 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:133.5,133.54 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:133.54,136.6 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:138.9,138.61 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:138.61,142.37 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:142.37,143.67 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:143.67,144.53 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:144.53,146.24 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:146.24,149.8 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:151.6,151.14 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:153.5,153.33 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:153.33,156.6 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:158.9,158.60 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:158.60,162.37 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:162.37,163.67 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:163.67,164.53 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:164.53,165.55 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:165.55,169.8 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:171.6,171.14 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:173.5,173.50 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:173.50,176.6 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:178.9,180.4 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:181.3,182.26 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:183.10,184.54 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:188.75,190.2 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:192.36,200.84 1 1 -github.com/echovault/echovault/internal/modules/admin/commands.go:200.84,204.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:213.84,217.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:225.86,229.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:238.86,242.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:252.86,256.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:267.84,271.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:272.73,273.49 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:273.49,275.6 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:276.5,276.45 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:285.84,289.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:290.73,292.18 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:292.18,294.6 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:295.5,295.53 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:304.84,308.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:309.73,310.47 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:310.47,312.6 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:313.5,313.45 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:321.84,325.5 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:335.86,339.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:340.75,341.34 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:341.34,343.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:344.7,345.34 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:345.34,347.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:348.7,348.75 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:348.75,350.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:351.7,351.47 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:360.86,364.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:365.75,366.35 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:366.35,368.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:369.7,370.47 2 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:379.86,383.7 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:384.75,387.38 3 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:387.38,389.8 1 0 -github.com/echovault/echovault/internal/modules/admin/commands.go:390.7,390.30 1 0 github.com/echovault/echovault/internal/modules/connection/commands.go:24.68,25.29 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:26.10,27.54 1 1 github.com/echovault/echovault/internal/modules/connection/commands.go:28.9,29.34 1 1 @@ -2109,8 +2018,8 @@ github.com/echovault/echovault/internal/modules/hash/commands.go:325.17,326.41 1 github.com/echovault/echovault/internal/modules/hash/commands.go:326.41,328.13 2 1 github.com/echovault/echovault/internal/modules/hash/commands.go:330.4,330.42 1 1 github.com/echovault/echovault/internal/modules/hash/commands.go:330.42,333.13 3 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:335.4,335.38 1 1 -github.com/echovault/echovault/internal/modules/hash/commands.go:335.38,337.13 2 1 +github.com/echovault/echovault/internal/modules/hash/commands.go:335.4,335.38 1 0 +github.com/echovault/echovault/internal/modules/hash/commands.go:335.38,337.13 2 0 github.com/echovault/echovault/internal/modules/hash/commands.go:342.2,342.25 1 1 github.com/echovault/echovault/internal/modules/hash/commands.go:345.68,347.16 2 1 github.com/echovault/echovault/internal/modules/hash/commands.go:347.16,349.3 1 1 @@ -4129,3 +4038,96 @@ github.com/echovault/echovault/internal/snapshot/snapshot.go:354.94,356.3 1 1 github.com/echovault/echovault/internal/snapshot/snapshot.go:358.2,360.12 2 1 github.com/echovault/echovault/internal/snapshot/snapshot.go:363.46,365.2 1 0 github.com/echovault/echovault/internal/snapshot/snapshot.go:367.42,369.2 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:27.78,33.29 4 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:33.29,34.54 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:34.54,40.42 4 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:40.42,42.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:44.4,47.12 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:50.3,50.36 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:50.36,57.43 5 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:57.43,59.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:61.4,63.21 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:67.2,69.25 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:72.76,76.35 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:76.35,77.65 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:77.65,78.41 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:78.41,80.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:81.4,81.12 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:83.3,83.13 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:86.2,86.51 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:89.75,90.29 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:91.9,96.36 4 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:96.36,97.66 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:97.66,98.52 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:98.52,102.6 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:103.5,103.13 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:105.4,106.14 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:108.3,109.26 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:111.9,115.56 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:115.56,117.4 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:118.3,118.53 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:118.53,122.37 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:122.37,123.67 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:123.67,124.53 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:124.53,125.59 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:125.59,129.8 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:131.6,131.14 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:133.5,133.54 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:133.54,136.6 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:138.9,138.61 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:138.61,142.37 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:142.37,143.67 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:143.67,144.53 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:144.53,146.24 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:146.24,149.8 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:151.6,151.14 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:153.5,153.33 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:153.33,156.6 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:158.9,158.60 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:158.60,162.37 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:162.37,163.67 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:163.67,164.53 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:164.53,165.55 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:165.55,169.8 3 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:171.6,171.14 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:173.5,173.50 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:173.50,176.6 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:178.9,180.4 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:181.3,182.26 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:183.10,184.54 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:188.75,190.2 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:192.36,200.84 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:200.84,204.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:213.84,217.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:225.86,229.7 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:238.86,242.7 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:252.86,256.7 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:267.84,271.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:272.73,273.49 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:273.49,275.6 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:276.5,276.45 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:285.84,289.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:290.73,292.18 2 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:292.18,294.6 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:295.5,295.53 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:304.84,308.5 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:309.73,310.47 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:310.47,312.6 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:313.5,313.45 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:321.84,325.5 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:335.86,339.7 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:340.75,341.34 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:341.34,343.8 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:344.7,345.34 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:345.34,347.8 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:348.7,348.75 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:348.75,350.8 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:351.7,351.47 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:360.86,364.7 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:365.75,366.35 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:366.35,368.8 1 0 +github.com/echovault/echovault/internal/modules/admin/commands.go:369.7,370.47 2 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:379.86,383.7 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:384.75,387.38 3 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:387.38,389.8 1 1 +github.com/echovault/echovault/internal/modules/admin/commands.go:390.7,390.30 1 1 diff --git a/echovault/echovault.go b/echovault/echovault.go index d4f9f921..a19b9028 100644 --- a/echovault/echovault.go +++ b/echovault/echovault.go @@ -357,15 +357,15 @@ func (server *EchoVault) startTCP() { if !conf.TLS { // TCP - fmt.Printf("Starting TCP echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) + log.Printf("Starting TCP echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) } if conf.TLS || conf.MTLS { // TLS if conf.TLS { - fmt.Printf("Starting mTLS echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) + log.Printf("Starting mTLS echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) } else { - fmt.Printf("Starting TLS echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) + log.Printf("Starting TLS echovault at Address %s, Port %d...\n", conf.BindAddr, conf.Port) } var certificates []tls.Certificate @@ -408,7 +408,7 @@ func (server *EchoVault) startTCP() { for { conn, err := listener.Accept() if err != nil { - fmt.Println("Could not establish connection") + log.Println("Could not establish connection") continue } // Read loop for connection diff --git a/echovault/plugin.go b/echovault/plugin.go index ee4c4f98..c3a070b1 100644 --- a/echovault/plugin.go +++ b/echovault/plugin.go @@ -33,6 +33,9 @@ import ( // `args` - ...string - A list of args that will be passed unmodified to the plugins command's // KeyExtractionFunc and HandlerFunc func (server *EchoVault) LoadModule(path string, args ...string) error { + server.commandsRWMut.Lock() + defer server.commandsRWMut.Unlock() + p, err := plugin.Open(path) if err != nil { return fmt.Errorf("plugin open: %v", err) @@ -104,9 +107,12 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { return errors.New("handler function has unexpected signature") } - server.commandsRWMut.Lock() - defer server.commandsRWMut.Unlock() + // Remove the currently loaded version of this module and replace it with the new one + server.commands = slices.DeleteFunc(server.commands, func(command internal.Command) bool { + return strings.EqualFold(command.Module, path) + }) + // Add the new command server.commands = append(server.commands, internal.Command{ Command: *command, Module: path, @@ -144,6 +150,7 @@ func (server *EchoVault) LoadModule(path string, args ...string) error { params.CreateKeyAndLock, params.GetValue, params.SetValue, + args..., ) }, }) diff --git a/internal/modules/admin/commands_test.go b/internal/modules/admin/commands_test.go index b0c99943..6a6f1cb9 100644 --- a/internal/modules/admin/commands_test.go +++ b/internal/modules/admin/commands_test.go @@ -17,35 +17,47 @@ package admin_test import ( "bytes" "context" + "errors" "fmt" "github.com/echovault/echovault/echovault" "github.com/echovault/echovault/internal" - "github.com/echovault/echovault/internal/config" "github.com/echovault/echovault/internal/constants" + "github.com/echovault/echovault/internal/modules/acl" + "github.com/echovault/echovault/internal/modules/admin" + "github.com/echovault/echovault/internal/modules/connection" + "github.com/echovault/echovault/internal/modules/generic" + "github.com/echovault/echovault/internal/modules/hash" + "github.com/echovault/echovault/internal/modules/list" + "github.com/echovault/echovault/internal/modules/pubsub" + "github.com/echovault/echovault/internal/modules/set" + "github.com/echovault/echovault/internal/modules/sorted_set" + str "github.com/echovault/echovault/internal/modules/string" "github.com/tidwall/resp" "net" + "os" + "path" "reflect" + "slices" "strings" + "sync" "testing" "unsafe" ) -var mockServer *echovault.EchoVault - -func init() { - mockServer, _ = echovault.NewEchoVault( - echovault.WithConfig(config.Config{ - DataDir: "", - EvictionPolicy: constants.NoEviction, - }), - ) +func setupServer(port uint16) (*echovault.EchoVault, error) { + cfg := echovault.DefaultConfig() + cfg.DataDir = "" + cfg.BindAddr = "localhost" + cfg.Port = port + cfg.EvictionPolicy = constants.NoEviction + return echovault.NewEchoVault(echovault.WithConfig(cfg)) } func getUnexportedField(field reflect.Value) interface{} { return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() } -func getHandler(commands ...string) internal.HandlerFunc { +func getHandler(mockServer *echovault.EchoVault, commands ...string) internal.HandlerFunc { if len(commands) == 0 { return nil } @@ -68,7 +80,7 @@ func getHandler(commands ...string) internal.HandlerFunc { return nil } -func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) internal.HandlerFuncParams { +func getHandlerFuncParams(ctx context.Context, mockServer *echovault.EchoVault, cmd []string, conn *net.Conn) internal.HandlerFuncParams { getCommands := getUnexportedField(reflect.ValueOf(mockServer).Elem().FieldByName("getCommands")).(func() []internal.Command) return internal.HandlerFuncParams{ @@ -79,19 +91,501 @@ func getHandlerFuncParams(ctx context.Context, cmd []string, conn *net.Conn) int } } -func Test_CommandsHandler(t *testing.T) { - res, err := getHandler("COMMANDS")(getHandlerFuncParams(context.Background(), []string{"commands"}, nil)) - if err != nil { - t.Error(err) - } +func Test_AdminCommand(t *testing.T) { + t.Cleanup(func() { + _ = os.RemoveAll("./testdata") + }) - rd := resp.NewReader(bytes.NewReader(res)) - rv, _, err := rd.ReadValue() - if err != nil { - t.Error(err) - } + t.Run("Test COMMANDS command", func(t *testing.T) { + t.Parallel() - for _, element := range rv.Array() { - fmt.Println(element) - } + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err := setupServer(uint16(port)) + if err != nil { + t.Error(err) + return + } + + res, err := getHandler(mockServer, "COMMANDS")( + getHandlerFuncParams(context.Background(), mockServer, []string{"commands"}, nil), + ) + if err != nil { + t.Error(err) + } + + rd := resp.NewReader(bytes.NewReader(res)) + rv, _, err := rd.ReadValue() + if err != nil { + t.Error(err) + } + + // Get all the commands from the existing modules. + var commands []internal.Command + commands = append(commands, acl.Commands()...) + commands = append(commands, admin.Commands()...) + commands = append(commands, generic.Commands()...) + commands = append(commands, hash.Commands()...) + commands = append(commands, list.Commands()...) + commands = append(commands, connection.Commands()...) + commands = append(commands, pubsub.Commands()...) + commands = append(commands, set.Commands()...) + commands = append(commands, sorted_set.Commands()...) + commands = append(commands, str.Commands()...) + + // Flatten the commands and subcommands. + var allCommands []string + for _, c := range commands { + if c.SubCommands == nil || len(c.SubCommands) == 0 { + allCommands = append(allCommands, c.Command) + continue + } + for _, sc := range c.SubCommands { + allCommands = append(allCommands, fmt.Sprintf("%s|%s", c.Command, sc.Command)) + } + } + + if len(allCommands) != len(rv.Array()) { + t.Errorf("expected commands list to be of length %d, got %d", len(allCommands), len(rv.Array())) + } + }) + + t.Run("Test MODULE LOAD command", func(t *testing.T) { + t.Parallel() + + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err := setupServer(uint16(port)) + if err != nil { + t.Error(err) + return + } + + tests := []struct { + name string + execCommand []resp.Value + wantExecRes string + wantExecErr error + testCommand []resp.Value + wantTestRes string + wantTestErr error + }{ + { + name: "1. Successfully load module_set module and return a response from the module handler", + execCommand: []resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("LOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_set", "module_set.so")), + }, + wantExecRes: "OK", + wantExecErr: nil, + testCommand: []resp.Value{ + resp.StringValue("MODULE.SET"), + resp.StringValue("key1"), + resp.StringValue("20"), + }, + wantTestRes: "OK", + wantTestErr: nil, + }, + { + name: "2. Successfully load module_get module and return a response from the module handler", + execCommand: []resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("LOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_get", "module_get.so")), + resp.StringValue("10"), // With args + }, + wantExecRes: "OK", + wantExecErr: nil, + testCommand: []resp.Value{ + resp.StringValue("MODULE.GET"), + resp.StringValue("key1"), + }, + wantTestRes: "200", + wantTestErr: nil, + }, + { + name: "3. Return error from module_set command handler", + execCommand: make([]resp.Value, 0), + wantExecRes: "", + wantExecErr: nil, + testCommand: []resp.Value{resp.StringValue("MODULE.SET"), resp.StringValue("key2")}, + wantTestRes: "", + wantTestErr: errors.New("wrong no of args for module.set command"), + }, + { + name: "4. Return error from module_get command handler", + execCommand: []resp.Value{ + resp.StringValue("SET"), + resp.StringValue("key2"), + resp.StringValue("value1"), + }, + wantExecRes: "OK", + wantExecErr: nil, + testCommand: []resp.Value{ + resp.StringValue("MODULE.GET"), + resp.StringValue("key2"), + }, + wantTestRes: "", + wantTestErr: errors.New("value at key key2 is not an integer"), + }, + { + name: "5. Return OK when reloading module that is already loaded", + execCommand: []resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("LOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_set", "module_set.so")), + }, + wantExecRes: "OK", + testCommand: []resp.Value{ + resp.StringValue("MODULE.SET"), + resp.StringValue("key3"), + resp.StringValue("20"), + }, + wantTestRes: "OK", + wantTestErr: nil, + }, + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() + + conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + if err != nil { + t.Error(err) + } + + respConn := resp.NewConn(conn) + + for i := 0; i < len(tests); i++ { + if len(tests[i].wantExecRes) > 0 { + // If the length of execCommand is > 0, write the command to the connection. + if err := respConn.WriteArray(tests[i].execCommand); err != nil { + t.Error(err) + } + // Read the response from the server. + r, _, err := respConn.ReadValue() + if err != nil { + t.Error(err) + } + // If we expect an error, check if the error matches the one we expect. + if tests[i].wantExecErr != nil { + if !strings.Contains(strings.ToLower(r.Error().Error()), strings.ToLower(tests[i].wantExecErr.Error())) { + t.Errorf("expected error to contain \"%s\", got \"%s\"", tests[i].wantExecErr.Error(), r.Error().Error()) + return + } + } + // If there's no expected error, check if the response is what's expected. + if tests[i].wantExecRes != "" { + if r.String() != tests[i].wantExecRes { + t.Errorf("expected exec response \"%s\", got \"%s\"", tests[i].wantExecRes, r.String()) + return + } + } + } + + if len(tests[i].testCommand) > 0 { + // If the length of test command is > 0, write teh command to the connections. + if err := respConn.WriteArray(tests[i].testCommand); err != nil { + t.Error(err) + } + // Read the response from the server. + r, _, err := respConn.ReadValue() + if err != nil { + t.Error(err) + } + // If we expect an error, check if the error is what's expected. + if tests[i].wantTestErr != nil { + if !strings.Contains(strings.ToLower(r.Error().Error()), strings.ToLower(tests[i].wantTestErr.Error())) { + t.Errorf("expected error to contain \"%s\", got \"%s\"", tests[i].wantTestErr.Error(), r.Error().Error()) + return + } + } + // Check if the response is what's expected. + if tests[i].wantTestRes != "" { + if r.String() != tests[i].wantTestRes { + t.Errorf("expected test response \"%s\", got \"%s\"", tests[i].wantTestRes, r.String()) + return + } + } + } + } + }) + + t.Run("Test MODULE UNLOAD command", func(t *testing.T) { + t.Parallel() + + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err := setupServer(uint16(port)) + if err != nil { + t.Error(err) + return + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() + + conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + if err != nil { + t.Error(err) + } + + respConn := resp.NewConn(conn) + + // Load module.set module + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("LOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_set", "module_set.so")), + }); err != nil { + t.Errorf("load module_set: %v", err) + return + } + // Expect OK response + r, _, err := respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if r.String() != "OK" { + t.Errorf("expected response OK, got \"%s\"", r.String()) + return + } + + // Load module.get module with arg + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("LOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_get", "module_get.so")), + resp.StringValue("10"), + }); err != nil { + t.Errorf("load module_get: %v", err) + return + } + // Expect OK response + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if r.String() != "OK" { + t.Errorf("expected response OK, got \"%s\"", r.String()) + return + } + + // Execute module.set command, expect OK response + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("module.set"), + resp.StringValue("key1"), + resp.StringValue("50"), + }); err != nil { + t.Errorf("exec module.set: %v", err) + return + } + // Expect OK response + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if r.String() != "OK" { + t.Errorf("expected response OK, got \"%s\"", r.String()) + return + } + + // Execute module.get command, expect integer response + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("module.get"), + resp.StringValue("key1"), + }); err != nil { + t.Errorf("exec module.get: %v", err) + return + } + // Expect integer response + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if r.Integer() != 500 { + t.Errorf("expected response 500, got \"%d\"", r.Integer()) + return + } + + // Unload module.set module + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("UNLOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_set", "module_set.so")), + }); err != nil { + t.Errorf("unload module_set: %v", err) + return + } + // Expect OK response + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if r.String() != "OK" { + t.Errorf("expected response OK, got \"%s\"", r.String()) + return + } + + // Unload module.get module + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("UNLOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_get", "module_get.so")), + }); err != nil { + t.Errorf("unload module_get: %v", err) + return + } + // Expect OK response + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if r.String() != "OK" { + t.Errorf("expected response OK, got \"%s\"", r.String()) + return + } + + // Try to execute module.set command, should receive command not supported error + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("module.set"), + resp.StringValue("key1"), + resp.StringValue("50"), + }); err != nil { + t.Errorf("retry module.set: %v", err) + return + } + // Expect command not supported response + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if !strings.Contains(r.Error().Error(), "command module.set not supported") { + t.Errorf("expected error to contain \"command module.set not supported\", got \"%s\"", r.Error().Error()) + return + } + + // Try to execute module.get command, should receive command not supported error + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("module.get"), + resp.StringValue("key1"), + }); err != nil { + t.Errorf("retry module.get: %v", err) + return + } + // Expect command not supported response + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if !strings.Contains(r.Error().Error(), "command module.get not supported") { + t.Errorf("expected error to contain \"command module.get not supported\", got \"%s\"", r.Error().Error()) + return + } + }) + + t.Run("Test MODULE LIST command", func(t *testing.T) { + t.Parallel() + + port, err := internal.GetFreePort() + if err != nil { + t.Error(err) + return + } + mockServer, err := setupServer(uint16(port)) + if err != nil { + t.Error(err) + return + } + + wg := sync.WaitGroup{} + wg.Add(1) + go func() { + wg.Done() + mockServer.Start() + }() + wg.Wait() + + conn, err := net.Dial("tcp", fmt.Sprintf("localhost:%d", port)) + if err != nil { + t.Error(err) + } + + respConn := resp.NewConn(conn) + + // Load module.get module with arg + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("LOAD"), + resp.StringValue(path.Join(".", "testdata", "modules", "module_get", "module_get.so")), + }); err != nil { + t.Errorf("load module_get: %v", err) + return + } + // Expect OK response + r, _, err := respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + if r.String() != "OK" { + t.Errorf("expected response OK, got \"%s\"", r.String()) + return + } + + if err := respConn.WriteArray([]resp.Value{ + resp.StringValue("MODULE"), + resp.StringValue("LIST"), + }); err != nil { + t.Errorf("list module: %v", err) + } + r, _, err = respConn.ReadValue() + if err != nil { + t.Error(err) + return + } + + serverModules := mockServer.ListModules() + + if len(r.Array()) != len(serverModules) { + t.Errorf("expected response of length %d, got %d", len(serverModules), len(r.Array())) + return + } + + for _, resModule := range r.Array() { + if !slices.ContainsFunc(serverModules, func(serverModule string) bool { + return resModule.String() == serverModule + }) { + t.Errorf("could not file module \"%s\" in the loaded server modules \"%s\"", resModule, serverModules) + } + } + }) } diff --git a/volumes/modules/module_get/module_get.go b/volumes/modules/module_get/module_get.go index 22187b69..8e10f1ef 100644 --- a/volumes/modules/module_get/module_get.go +++ b/volumes/modules/module_get/module_get.go @@ -16,7 +16,9 @@ package main import ( "context" + "errors" "fmt" + "strconv" ) var Command string = "Module.Get" @@ -69,5 +71,13 @@ func HandlerFunc( return nil, fmt.Errorf("value at key %s is not an integer", key) } - return []byte(fmt.Sprintf(":%d\r\n", val*val)), nil + factor := val + if len(args) >= 1 { + factor, err = strconv.ParseInt(args[0], 10, 64) + if err != nil { + return nil, errors.New("first value of args must be an integer") + } + } + + return []byte(fmt.Sprintf(":%d\r\n", val*factor)), nil } diff --git a/volumes/modules/module_get/module_get.so b/volumes/modules/module_get/module_get.so index ff1accb2..6c00d781 100644 Binary files a/volumes/modules/module_get/module_get.so 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 index 2c88f407..87bdd111 100644 --- a/volumes/modules/module_set/module_set.go +++ b/volumes/modules/module_set/module_set.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "strconv" + "strings" ) var Command string = "Module.Set" @@ -31,7 +32,7 @@ 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 nil, nil, fmt.Errorf("wrong no of args for %s command", strings.ToLower(Command)) } return []string{}, cmd[1:2], nil } diff --git a/volumes/modules/module_set/module_set.so b/volumes/modules/module_set/module_set.so index 996b9912..445f882c 100644 Binary files a/volumes/modules/module_set/module_set.so and b/volumes/modules/module_set/module_set.so differ