diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b75f6ea --- /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 Dockerfile docker-compose.yaml go.mod main.go /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..f44dc73 --- /dev/null +++ b/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "log" + "time" + "net/http" + "fmt" + + "github.com/gin-gonic/gin" + "github.com/go-redis/redis/v8" + "github.com/google/uuid" +) + +var client *redis.Client +var hllKey = "ping_callers" + + +func init() { + // Initialize Redis client + client = redis.NewClient(&redis.Options{ + Addr: "redis:6379", + }) +} + +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) { + 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() + + // Store the session ID and user name in Redis + err := client.Set(c, sessionID, username, 0).Err() + if err != nil { + log.Println(err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server Error"}) + return + } + + // 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, 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 == 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 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 + } + + 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) + + 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 { + return uuid.New().String() +} \ No newline at end of file