From aa08c1b10e988370d8a9d8e7656de036addc9f27 Mon Sep 17 00:00:00 2001 From: dodx Date: Fri, 2 Jun 2023 15:32:15 +0700 Subject: [PATCH 1/2] update code solution --- Dockerfile | 15 +++++ docker-compose.yaml | 12 ++++ main.go | 138 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yaml create mode 100644 main.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4345f2e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# Sử dụng ảnh golang:1.16.0-alpine làm base image +FROM golang:latest + +# Sao chép mã nguồn vào thư mục /app trong container +COPY . /app + +# Đặt thư mục làm việc mặc định trong container +WORKDIR /app + +RUN go get github.com/EngineerProOrg/BE-K01 +# Cài đặt dependencies và biên dịch ứng dụng +RUN go mod download && go build -o main . + +# Chạy ứng dụng +CMD ["./main"] \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..3aaf1aa --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,12 @@ +version: '3' +services: + redis: + image: redis:latest + ports: + - 6379:6379 + app: + build: . + ports: + - 8080:8080 + depends_on: + - redis diff --git a/main.go b/main.go new file mode 100644 index 0000000..66a1357 --- /dev/null +++ b/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "log" + "strconv" + "sync" + "time" + "net/http" + "fmt" + + "github.com/gin-gonic/gin" + "github.com/go-redis/redis/v8" + "github.com/golang/groupcache/lru" +) + +var client *redis.Client +var mutex = &sync.Mutex{} +var lruCache *lru.Cache +var hllKey = "ping_callers" + + +func init() { + // Initialize Redis client + client = redis.NewClient(&redis.Options{ + Addr: "redis:6379", + }) + + // Initialize LRU cache with a maximum of 10 entries + lruCache = lru.New(10) +} + +func main() { + + // Check to connect Redis + _, err := client.Ping(client.Context()).Result() + if err != nil { + log.Fatal(err) + } + + router := gin.Default() + + router.POST("/login", loginHandler) + router.GET("/ping", pingHandler) + router.GET("/top", topHandler) + router.GET("/count", countHandler) + + router.Run(":8080") +} + +func loginHandler(c *gin.Context) { + username := c.PostForm("username") + sessionID := generateSessionID() + + // Store the session ID and user name in Redis + // err := client.HSet(client.Context(), "sessions", sessionID, username).Err() + err := client.Set(c, sessionID, username, 0).Err() + fmt.Println(err) + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) + return + } + + // c.JSON(200, gin.H{"username": username, "session_id": sessionID}) + c.JSON(http.StatusOK, gin.H{"session_id": sessionID}) +} + +func pingHandler(c *gin.Context) { + sessionID := c.Query("session_id") + + // Check if the session ID is valid + userName, err := client.Get(c, sessionID).Result() + if err == redis.Nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session ID"}) + return + } else if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve session"}) + return + } + + // Acquire lock to ensure only one person can call /ping at a time + mutex.Lock() + defer mutex.Unlock() + + // Check if the user has exceeded the rate limit + callCountKey := fmt.Sprintf("call_count:%s", userName) + callCount, _ := lruCache.Get(callCountKey) + if callCount == nil { + callCount = 1 + } else { + count, _ := strconv.Atoi(callCount.(string)) + if count >= 2 { + c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"}) + return + } + callCount = count + 1 + } + + // Increment the call count + lruCache.Add(callCountKey, callCount) + + // Sleep for 5 seconds to simulate processing time + time.Sleep(5 * time.Second) + + c.JSON(http.StatusOK, gin.H{"message": "pong"}) +} + +func topHandler(c *gin.Context) { + // Retrieve the top 10 most-called /ping APIs + result, err := client.ZRevRangeWithScores(c, hllKey, 0, 9).Result() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve top APIs"}) + return + } + + var topAPIs []string + for _, z := range result { + topAPIs = append(topAPIs, z.Member.(string)) + } + + c.JSON(http.StatusOK, gin.H{"top_apis": topAPIs}) +} + +func countHandler(c *gin.Context) { + // Retrieve the approximate number of callers to /ping API using HyperLogLog + count, err := client.PFCount(c, hllKey).Result() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to retrieve count"}) + return + } + + c.JSON(http.StatusOK, gin.H{"count": count}) +} + +func generateSessionID() string { + timestamp := time.Now().UnixNano() + return fmt.Sprintf("session:%d", timestamp) +} \ No newline at end of file From cb23fcb39d50b65a82e30a1e9444719c20e9ba20 Mon Sep 17 00:00:00 2001 From: dodx Date: Fri, 9 Jun 2023 17:53:55 +0700 Subject: [PATCH 2/2] fix error follow instruction by Dong Truong --- Dockerfile | 2 +- main.go | 77 +++++++++++++++++++++++++++++------------------------- 2 files changed, 43 insertions(+), 36 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4345f2e..b75f6ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM golang:latest # Sao chép mã nguồn vào thư mục /app trong container -COPY . /app +COPY Dockerfile docker-compose.yaml go.mod main.go /app/ # Đặt thư mục làm việc mặc định trong container WORKDIR /app diff --git a/main.go b/main.go index 66a1357..f44dc73 100644 --- a/main.go +++ b/main.go @@ -2,20 +2,16 @@ package main import ( "log" - "strconv" - "sync" "time" "net/http" "fmt" "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" - "github.com/golang/groupcache/lru" + "github.com/google/uuid" ) var client *redis.Client -var mutex = &sync.Mutex{} -var lruCache *lru.Cache var hllKey = "ping_callers" @@ -24,9 +20,6 @@ func init() { client = redis.NewClient(&redis.Options{ Addr: "redis:6379", }) - - // Initialize LRU cache with a maximum of 10 entries - lruCache = lru.New(10) } func main() { @@ -48,29 +41,40 @@ func main() { } func loginHandler(c *gin.Context) { + sessionID := c.GetHeader("session_id") + if sessionID != "" { + // Session ID already exists, no need to create a new session + c.JSON(http.StatusOK, gin.H{"message": "Already logged in"}) + return + } + username := c.PostForm("username") - sessionID := generateSessionID() + sessionID = generateSessionID() // Store the session ID and user name in Redis - // err := client.HSet(client.Context(), "sessions", sessionID, username).Err() err := client.Set(c, sessionID, username, 0).Err() - fmt.Println(err) if err != nil { log.Println(err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) return } - // c.JSON(200, gin.H{"username": username, "session_id": sessionID}) - c.JSON(http.StatusOK, gin.H{"session_id": sessionID}) + // Set sessionID cookie + c.SetCookie("session_id", sessionID, 300, "/", "localhost", false, true) + + c.JSON(http.StatusOK, gin.H{"session_id": sessionID, "username": username, "message": "Session created successfully"}) } func pingHandler(c *gin.Context) { - sessionID := c.Query("session_id") + sessionID, err := c.Cookie("session_id") + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "session ID not found"}) + return + } // Check if the session ID is valid userName, err := client.Get(c, sessionID).Result() - if err == redis.Nil { + if err == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid session ID"}) return } else if err != nil { @@ -78,26 +82,30 @@ func pingHandler(c *gin.Context) { return } - // Acquire lock to ensure only one person can call /ping at a time - mutex.Lock() - defer mutex.Unlock() - - // Check if the user has exceeded the rate limit - callCountKey := fmt.Sprintf("call_count:%s", userName) - callCount, _ := lruCache.Get(callCountKey) - if callCount == nil { - callCount = 1 - } else { - count, _ := strconv.Atoi(callCount.(string)) - if count >= 2 { - c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"}) - return - } - callCount = count + 1 + // Acquire distributed lock to ensure only one person can call /ping at a time + lockKey := fmt.Sprintf("lock:%s", userName) + ok, err := client.SetNX(c, lockKey, "locked", 5*time.Second).Result() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to obtain lock"}) + return + } + if !ok { + c.JSON(http.StatusTooManyRequests, gin.H{"error": "failed to obtain lock"}) + return + } + + // Implement Redis-based rate limiting + rateLimitKey := fmt.Sprintf("ratelimit:%s", userName) + remaining, err := client.Incr(c, rateLimitKey).Result() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to increment rate limit"}) + return } - // Increment the call count - lruCache.Add(callCountKey, callCount) + if remaining > 2 { + c.JSON(http.StatusTooManyRequests, gin.H{"error": "rate limit exceeded"}) + return + } // Sleep for 5 seconds to simulate processing time time.Sleep(5 * time.Second) @@ -133,6 +141,5 @@ func countHandler(c *gin.Context) { } func generateSessionID() string { - timestamp := time.Now().UnixNano() - return fmt.Sprintf("session:%d", timestamp) + return uuid.New().String() } \ No newline at end of file