diff --git a/README.md b/README.md index 85c23b3..4f10e3a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,13 @@ # Key Value DB (Redis v0.0.1) in Go -This readme file provides an overview and instructions for using the keyvaluedb CLI tool written in Golang. - +This repository serves as a reference implementation for the problem statement described [here](https://playbook.one2n.in/Key-Value-DB-Redis-exercise). ## Description The CLI tool is a key-value database (KVDB) that allows users to interact with a simple in-memory database through a TCP server. It provides a command-line interface for executing various commands and retrieving results. ## Installation -To use the CLI tool, you need to have Golang(1.18) installed on your machine. Follow the steps below to install and set up the tool: +To use the CLI tool, you need to have Go(1.18) installed on your machine. Follow the steps below to install and set up the tool: 1. Clone the repository or download the source code files. 2. Open a terminal and navigate to the project directory. @@ -26,23 +25,26 @@ To use the CLI tool, you need to have Golang(1.18) installed on your machine. Fo To start the TCP server and use the CLI tool, follow these steps: -1. Ensure the environment variable `APP_PORT` is set to the desired port number on which the TCP server should listen. For example, you can set it to `9736` by running: - ```shell - export APP_PORT=9736 - ``` +1. Create `.env` file in the root directory and set the desired values to the below environment variables. We have provided a `.env.example` file for reference. - Replace `9736` with the desired port number. + 1. Ensure the environment variable `APP_PORT` is set to the desired port number on which the TCP server should listen. For example, you can set it to `9736` by running: -2. Optionally, if you want to specify the number of in-memory databases (`DB_COUNT`), set the environment variable as well. For example: + ```shell + export APP_PORT=9736 + ``` - ```shell - export DB_COUNT=16 - ``` + Replace `9736` with the desired port number. + + 2. Optionally, if you want to specify the number of in-memory databases (`DB_COUNT`), set the environment variable as well. For example: + + ```shell + export DB_COUNT=16 + ``` - Replace `16` with the desired number of databases. If not set, the default value is `16`. + Replace `16` with the desired number of databases. If not set, the default value is `16`. -3. Run the following command to start the TCP server: +2. Run the following command to start the TCP server: ```shell ./kvdb @@ -50,9 +52,9 @@ To start the TCP server and use the CLI tool, follow these steps: Replace `./kvdb` with the actual path to the `kvdb` executable if it's not in the current directory or not in your `PATH`. -4. The TCP server will start and display a message indicating that it is listening on the specified port. +3. The TCP server will start and display a message indicating that it is listening on the specified port. -5. Open another terminal or use a tool like `nc` to connect to the TCP server. For example: +4. Open another terminal or use a tool like `nc` to connect to the TCP server. For example: ```shell nc localhost 9736 @@ -60,9 +62,9 @@ To start the TCP server and use the CLI tool, follow these steps: Replace `localhost` with the appropriate host if the server is running on a different machine, and `9736` with the correct port number. -6. Once connected, you can start interacting with the CLI tool by entering commands. The command prompt is denoted by a `$` symbol. +5. Once connected, you can start interacting with the CLI tool by entering commands. The command prompt is denoted by a `$` symbol. -7. The available commands are case-insensitive and can be entered in the following format: +6. The available commands are case-insensitive and can be entered in the following format: ``` COMMAND [argument1] [argument2] ... @@ -70,7 +72,7 @@ To start the TCP server and use the CLI tool, follow these steps: Replace `COMMAND` with one of the supported commands and provide the necessary arguments. -8. The CLI tool supports the following commands: +7. The CLI tool supports the following commands: - `SET key value`: Sets the value of the specified key in the current database. - `GET key`: Retrieves the value of the specified key from the current database. @@ -85,9 +87,9 @@ To start the TCP server and use the CLI tool, follow these steps: Replace key, value, index, and increment with the appropriate values. -9. After entering a command, the CLI tool will display the result of the command. If the result is a list, each item will be numbered. +8. After entering a command, the CLI tool will display the result of the command. If the result is a list, each item will be numbered. -10. To exit the CLI tool, close the `nc` connection or terminate the terminal session. +9. To exit the CLI tool, close the `nc` connection or terminate the terminal session. ## Dependencies diff --git a/domain/keyvaluedb.go b/domain/keyvaluedb.go index 7fd152e..9c5b534 100644 --- a/domain/keyvaluedb.go +++ b/domain/keyvaluedb.go @@ -19,19 +19,19 @@ func NewKeyValueDB(storage storage.Storage) KeyValueDB { func (kvdb *KeyValueDB) Execute(dbIndex int, cmd Command) (int, interface{}) { _, err := cmd.Validate() if err != nil { - return 0, err.Error() + return dbIndex, err.Error() } if kvdb.isMultiBlockStarted && !cmd.isTerminatorCmd() { kvdb.enqueue(cmd) - return 0, "QUEUED" + return dbIndex, "QUEUED" } switch cmd.Name { case SELECT: dbIndex, err = kvdb.storage.Select(cmd.Key) if err != nil { - return 0, err.Error() + return dbIndex, err.Error() } return dbIndex, "OK" case MULTI: diff --git a/main.go b/main.go index a5853f8..d9c85ef 100644 --- a/main.go +++ b/main.go @@ -8,12 +8,18 @@ import ( "log" "net" "os" + "os/signal" "strings" + "syscall" "github.com/joho/godotenv" ) func main() { + + // Handle interrupt signal + handleInterruptSignal() + err := godotenv.Load() if err != nil { log.Fatal(err.Error()) @@ -42,6 +48,19 @@ func main() { } } +func handleInterruptSignal() { + // Create an interrupt channel to listen for the interrupt signal + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM) + + go func() { + <-interrupt + fmt.Println("Interrupt signal received. Gracefully stopping...") + + os.Exit(0) + }() +} + func startTcpServer(port string) (net.Listener, error) { listener, err := net.Listen("tcp", port) if err != nil { diff --git a/storage/inmemory_storage_test.go b/storage/inmemory_storage_test.go new file mode 100644 index 0000000..7b97c04 --- /dev/null +++ b/storage/inmemory_storage_test.go @@ -0,0 +1,268 @@ +package storage + +import ( + "fmt" + "reflect" + "testing" +) + +func TestNewInMemory(t *testing.T) { + tests := []struct { + name string + dbCntStr string + want Storage + }{ + { + name: "Empty dbCountStr should default to 16", + dbCntStr: "", + want: NewInMemory("16"), + }, + { + name: "Valid dbCountStr", + dbCntStr: "10", + want: NewInMemory("10"), + }, + { + name: "Invalid dbCountStr should default to 16", + dbCntStr: "abc", + want: NewInMemory("16"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewInMemory(tt.dbCntStr) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewInMemory() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestInMemorySelect(t *testing.T) { + tests := []struct { + name string + dbCntStr string + dbIndexStr string + want int + wantErr error + }{ + { + name: "Valid dbIndexStr within range", + dbCntStr: "16", + dbIndexStr: "0", + want: 0, + wantErr: nil, + }, + { + name: "Valid dbIndexStr within range", + dbCntStr: "16", + dbIndexStr: "10", + want: 10, + wantErr: nil, + }, + { + name: "Invalid dbIndexStr (out of range)", + dbCntStr: "16", + dbIndexStr: "-1", + want: 0, + wantErr: fmt.Errorf("(error) ERR DB index is out of range"), + }, + { + name: "Invalid dbIndexStr (not an integer)", + dbCntStr: "16", + dbIndexStr: "abc", + want: 0, + wantErr: fmt.Errorf("(error) ERR value is not an integer or out of range"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in := NewInMemory(tt.dbCntStr) + + got, err := in.Select(tt.dbIndexStr) + if err != nil { + if tt.wantErr == nil { + t.Errorf("inMemory.Select() error = %v, wantErr ", err) + } else if err.Error() != tt.wantErr.Error() { + t.Errorf("inMemory.Select() error = %v, wantErr %v", err, tt.wantErr) + } + } else if tt.wantErr != nil { + t.Errorf("inMemory.Select() error = , wantErr %v", tt.wantErr) + } + + if got != tt.want { + t.Errorf("inMemory.Select() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestInMemorySetGet(t *testing.T) { + type args struct { + dbIndex int + key string + value interface{} + } + tests := []struct { + name string + dbCntStr string + key string + setArgs args + want interface{} + }{ + { + name: "Set a new key-value pair", + setArgs: args{ + dbIndex: 0, + key: "key1", + value: "value1", + }, + key: "key1", + want: "value1", + }, + { + name: "Update the value of an existing key", + setArgs: args{ + dbIndex: 0, + key: "key1", + value: "value2", + }, + key: "key1", + want: "value2", + }, + { + name: "Get a value for nonexisting key", + key: "nonexisting", + want: nil, + }, + { + name: "Get the value of an existing key", + setArgs: args{ + dbIndex: 0, + key: "key1", + value: "value2", + }, + key: "key1", + want: "value2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in := NewInMemory(tt.dbCntStr) + + in.Set(tt.setArgs.dbIndex, tt.setArgs.key, tt.setArgs.value) + + got := in.Get(tt.setArgs.dbIndex, tt.key) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("inMemory.Set() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestInMemorySetDel(t *testing.T) { + type args struct { + dbIndex int + key string + value interface{} + } + tests := []struct { + name string + dbCntStr string + key string + setArgs args + want interface{} + }{ + { + name: "Del a value for nonexisting key", + key: "nonexisting", + want: 0, + }, + { + name: "Del the value of an existing key", + setArgs: args{ + dbIndex: 0, + key: "key1", + value: "value2", + }, + key: "key1", + want: 1, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in := NewInMemory(tt.dbCntStr) + + in.Set(tt.setArgs.dbIndex, tt.setArgs.key, tt.setArgs.value) + + got := in.Del(tt.setArgs.dbIndex, tt.key) + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("inMemory.Set() = %v, want %v", got, tt.want) + } + + if in.Get(tt.setArgs.dbIndex, tt.key) != nil { + t.Errorf("Del(%d, %s) did not delete the key properly", tt.setArgs.dbIndex, tt.key) + } + }) + } +} + +func TestInMemoryGetAll(t *testing.T) { + type fields struct { + key string + value interface{} + } + tests := []struct { + name string + dbCntStr string + dbIndex int + fields []fields + wantAll []string + }{ + { + name: "Get all key-value pairs", + dbCntStr: "1", + dbIndex: 0, + fields: []fields{ + { + key: "key1", + value: "value1", + }, + { + key: "key2", + value: "value2", + }, + { + key: "key3", + value: "value3", + }, + }, + wantAll: []string{"key1 value1", "key2 value2", "key3 value3"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + in := NewInMemory(tt.dbCntStr) + + for _, f := range tt.fields { + in.Set(tt.dbIndex, f.key, f.value) + } + + allChan := in.GetAll(tt.dbIndex) + + var gotAll []string + for s := range allChan { + gotAll = append(gotAll, s) + } + if len(gotAll) != len(tt.wantAll) { + t.Errorf("GetAll(%d) returned %d items, want %d items", tt.dbIndex, len(gotAll), len(tt.wantAll)) + } + for i, got := range gotAll { + if got != tt.wantAll[i] { + t.Errorf("GetAll(%d) returned %s, want %s", tt.dbIndex, got, tt.wantAll[i]) + } + } + }) + } +}