Skip to content

Commit

Permalink
Refactoring HTTP server (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucifercr07 authored Sep 28, 2024
1 parent 98b5742 commit fb99200
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 150 deletions.
43 changes: 0 additions & 43 deletions internal/api/api.go

This file was deleted.

12 changes: 6 additions & 6 deletions internal/db/commands.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package db

func getKey(key string) (string, error) {
val, err := rdb.Get(ctx, key).Result()
func (db *DiceDB) getKey(key string) (string, error) {
val, err := db.Client.Get(db.Ctx, key).Result()
return val, err
}

func setKey(key, value string) error {
err := rdb.Set(ctx, key, value, 0).Err()
func (db *DiceDB) setKey(key, value string) error {
err := db.Client.Set(db.Ctx, key, value, 0).Err()
return err
}

func deleteKeys(keys []string) error {
err := rdb.Del(ctx, keys...).Err()
func (db *DiceDB) deleteKeys(keys []string) error {
err := db.Client.Del(db.Ctx, keys...).Err()
return err
}
63 changes: 49 additions & 14 deletions internal/db/dicedb.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,93 @@ package db

import (
"context"
"log"
"errors"
"fmt"
"log/slog"
"os"
"server/config"
"server/internal/cmds"
"time"

dice "github.com/dicedb/go-dice"
)

var rdb *dice.Client
var ctx = context.Background()
type DiceDB struct {
Client *dice.Client
Ctx context.Context
}

func CloseDiceDB() {
err := rdb.Close()
func (db *DiceDB) CloseDiceDB() {
err := db.Client.Close()
if err != nil {
log.Fatalf("error closing DiceDB connection: %v", err)
slog.Error("error closing DiceDB connection",
slog.Any("error", err))
os.Exit(1)
}
}

func InitDiceClient(configValue *config.Config) (*DiceDB, error) {
diceClient := dice.NewClient(&dice.Options{
Addr: configValue.DiceAddr,
DialTimeout: 10 * time.Second,
MaxRetries: 10,
})

// Ping the dice client to verify the connection
err := diceClient.Ping(context.Background()).Err()
if err != nil {
return nil, err
}

return &DiceDB{
Client: diceClient,
Ctx: context.Background(),
}, nil
}

func errorResponse(response string) map[string]string {
return map[string]string{"error": response}
}

func ExecuteCommand(command *cmds.CommandRequest) interface{} {
// ExecuteCommand executes a command based on the input
func (db *DiceDB) ExecuteCommand(command *cmds.CommandRequest) interface{} {
switch command.Cmd {
case "get":
if command.Args.Key == "" {
return errorResponse("key is required")
}
val, err := getKey(command.Args.Key)
if err != nil {
return errorResponse("error running get command")

val, err := db.getKey(command.Args.Key)
switch {
case errors.Is(err, dice.Nil):
return errorResponse("key does not exist")
case err != nil:
return errorResponse(fmt.Sprintf("Get failed %v", err))
}

return map[string]string{"value": val}

case "set":
if command.Args.Key == "" || command.Args.Value == "" {
return errorResponse("key and value are required")
}
err := setKey(command.Args.Key, command.Args.Value)
err := db.setKey(command.Args.Key, command.Args.Value)
if err != nil {
return errorResponse("failed to set key")
}
return map[string]string{"result": "OK"}

case "del":
if len(command.Args.Keys) == 0 {
return map[string]string{"error": "atleast one key is required"}
return errorResponse("at least one key is required")
}
err := deleteKeys(command.Args.Keys)
err := db.deleteKeys(command.Args.Keys)
if err != nil {
return map[string]string{"error": "failed to delete keys"}
return errorResponse("failed to delete keys")
}

return map[string]string{"result": "OK"}

default:
return errorResponse("unknown command")
}
Expand Down
27 changes: 12 additions & 15 deletions internal/middleware/ratelimiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,38 @@ package middleware

import (
"context"
"errors"
"fmt"
"log/slog" // Import the slog package for structured logging
"log/slog"
"net/http"
"server/internal/db"
"strconv"
"strings"
"time"

dice "github.com/dicedb/go-dice" // Import dice package
dice "github.com/dicedb/go-dice"
)

// RateLimiter middleware to limit requests based on a specified limit and duration
func RateLimiter(diceClient *dice.Client, next http.Handler, limit, window int) http.Handler {
func RateLimiter(client *db.DiceDB, next http.Handler, limit, window int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Check DiceDB connection health
if err := diceClient.Ping(ctx).Err(); err != nil {
slog.Error("DiceDB connection is down", "error", err)
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
return
}

// Skip rate limiting for non-command endpoints
if r.URL.Path != "/command" {
if !strings.Contains(r.URL.Path, "/cli/") {
next.ServeHTTP(w, r)
return
}

// Get the current time window as a unique key
currentWindow := time.Now().Unix() / int64(window)
key := fmt.Sprintf("request_count:%d", currentWindow)
slog.Info("Created rate limiter key", slog.Any("key", key))

// Fetch the current request count
val, err := diceClient.Get(ctx, key).Result()
if err != nil && err != dice.Nil {
val, err := client.Client.Get(ctx, key).Result()
if err != nil && !errors.Is(err, dice.Nil) {
slog.Error("Error fetching request count", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
Expand All @@ -61,15 +58,15 @@ func RateLimiter(diceClient *dice.Client, next http.Handler, limit, window int)
}

// Increment the request count
if _, err := diceClient.Incr(ctx, key).Result(); err != nil {
if _, err := client.Client.Incr(ctx, key).Result(); err != nil {
slog.Error("Error incrementing request count", "error", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

// Set the key expiry if it's newly created
if requestCount == 0 {
if err := diceClient.Expire(ctx, key, time.Duration(window)*time.Second).Err(); err != nil {
if err := client.Client.Expire(ctx, key, time.Duration(window)*time.Second).Err(); err != nil {
slog.Error("Error setting expiry for request count", "error", err)
}
}
Expand Down
90 changes: 90 additions & 0 deletions internal/server/httpServer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package server

import (
"context"
"errors"
"log"
"net/http"
"strings"
"sync"
"time"

"server/internal/db"
util "server/pkg/util"
)

type HTTPServer struct {
httpServer *http.Server
DiceClient *db.DiceDB
}

// HandlerMux wraps ServeMux and forces REST paths to lowercase
// and attaches a rate limiter with the handler
type HandlerMux struct {
mux *http.ServeMux
}

func (cim *HandlerMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Convert the path to lowercase before passing to the underlying mux.
r.URL.Path = strings.ToLower(r.URL.Path)
cim.mux.ServeHTTP(w, r)
}

func NewHTTPServer(addr string, mux *http.ServeMux, client *db.DiceDB) *HTTPServer {
caseInsensitiveMux := &HandlerMux{
mux: mux,
}

return &HTTPServer{
httpServer: &http.Server{
Addr: addr,
Handler: caseInsensitiveMux,
ReadHeaderTimeout: 5 * time.Second,
},
DiceClient: client,
}
}

func (s *HTTPServer) Run(ctx context.Context) error {
var wg sync.WaitGroup

wg.Add(1)
go func() {
defer wg.Done()
log.Printf("Starting server at %s\n", s.httpServer.Addr)
if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
}()

<-ctx.Done()
log.Println("Shutting down server...")
return s.Shutdown()
}

func (s *HTTPServer) Shutdown() error {
if err := s.DiceClient.Client.Close(); err != nil {
log.Printf("Failed to close dice client: %v", err)
}

return s.httpServer.Shutdown(context.Background())
}

func (s *HTTPServer) HealthCheck(w http.ResponseWriter, request *http.Request) {
util.JSONResponse(w, http.StatusOK, map[string]string{"message": "Server is running"})
}

func (s *HTTPServer) CliHandler(w http.ResponseWriter, r *http.Request) {
diceCmds, err := util.ParseHTTPRequest(r)
if err != nil {
http.Error(w, "Error parsing HTTP request", http.StatusBadRequest)
return
}

resp := s.DiceClient.ExecuteCommand(diceCmds)
util.JSONResponse(w, http.StatusOK, resp)
}

func (s *HTTPServer) SearchHandler(w http.ResponseWriter, request *http.Request) {
util.JSONResponse(w, http.StatusOK, map[string]string{"message": "Search results"})
}
Loading

0 comments on commit fb99200

Please sign in to comment.